package com.example.memento.download.service;

import com.example.memento.download.config.DownloadProperties;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.commons.compress.utils.IOUtils;
import org.springframework.stereotype.Service;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

@Service
public class DownloadService {
    private final DownloadProperties properties;
    private final GithubProxyService proxyService;
    private final HttpClient httpClient;

    public DownloadService(DownloadProperties properties, GithubProxyService proxyService) {
        this.properties = properties;
        this.proxyService = proxyService;
        this.httpClient = createHttpClient();
    }
    
    /**
     * 创建 HttpClient，根据配置决定是否跳过 SSL 验证
     */
    private HttpClient createHttpClient() {
        HttpClient.Builder builder = HttpClient.newBuilder()
                .followRedirects(HttpClient.Redirect.ALWAYS)
                .connectTimeout(Duration.ofSeconds(20))
                .executor(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()));
        
        // 如果配置了跳过 SSL 验证，则使用自定义的 SSL 上下文
        if (properties.isSkipSslVerification()) {
            try {
                SSLContext sslContext = createTrustAllSslContext();
                builder.sslContext(sslContext);
            } catch (Exception e) {
                throw new RuntimeException("无法创建 SSL 上下文", e);
            }
        }
        
        return builder.build();
    }
    
    /**
     * 创建一个信任所有证书的 SSL 上下文（仅用于开发/测试环境）
     * 警告：这会跳过 SSL 证书验证，存在安全风险
     */
    private SSLContext createTrustAllSslContext() throws NoSuchAlgorithmException, KeyManagementException {
        TrustManager[] trustAllCerts = new TrustManager[]{
            new X509TrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] chain, String authType) {
                    // 信任所有客户端证书
                }
                
                @Override
                public void checkServerTrusted(X509Certificate[] chain, String authType) {
                    // 信任所有服务器证书
                }
                
                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }
            }
        };
        
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
        return sslContext;
    }

    public List<Path> downloadAll(List<String> urls, Path targetDir, boolean extract) throws IOException {
        Path base = targetDir != null ? targetDir : Path.of(properties.getBaseDir());
        Files.createDirectories(base);

        // 转换 GitHub URL 为代理 URL（如果需要）
        List<UrlInfo> processedUrls = processUrls(urls);

        List<CompletableFuture<Path>> futures = new ArrayList<>();
        for (UrlInfo urlInfo : processedUrls) {
            futures.add(CompletableFuture.supplyAsync(() -> {
                try {
                    // 传递 UrlInfo 以便处理回退逻辑
                    return downloadSingle(urlInfo, base, extract);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }));
        }

        return futures.stream().map(CompletableFuture::join).toList();
    }

    /**
     * 处理 URL 列表，如果是 GitHub URL 且需要代理，则获取所有可能的代理 URL（按优先级）
     * 返回 URL 列表信息，支持多代理降级尝试
     */
    private List<UrlInfo> processUrls(List<String> urls) {
        // 检查是否需要使用代理
        boolean useProxy = proxyService.shouldUseProxy();
        
        return urls.stream()
                .map(originalUrl -> {
                    List<String> candidateUrls = new java.util.ArrayList<>();
                    
                    if (useProxy && proxyService.isGitHubUrl(originalUrl)) {
                        // 获取所有代理 URL（包括原始 URL 作为兜底）
                        candidateUrls = proxyService.getProxyUrlsWithFallback(originalUrl);
                    } else {
                        // 不使用代理，直接使用原始 URL
                        candidateUrls.add(originalUrl);
                    }
                    
                    // 第一个 URL 作为初始尝试
                    String firstUrl = candidateUrls.isEmpty() ? originalUrl : candidateUrls.get(0);
                    
                    return new UrlInfo(firstUrl, originalUrl, candidateUrls, 
                            useProxy && proxyService.isGitHubUrl(originalUrl));
                })
                .toList();
    }
    
    /**
     * URL 信息类，保存候选 URL 列表（按优先级排序，最后包含原始 URL 作为兜底）
     */
    private static class UrlInfo {
        final List<String> candidateUrls;  // 所有候选 URL（包括原始 URL 作为兜底）
        
        UrlInfo(String url, String originalUrl, List<String> candidateUrls, boolean hasProxies) {
            this.candidateUrls = candidateUrls != null ? candidateUrls : java.util.Collections.singletonList(originalUrl);
        }
    }

    private Path downloadSingle(UrlInfo urlInfo, Path baseDir, boolean extract) throws IOException {
        return downloadSingleWithFallback(urlInfo, baseDir, extract);
    }
    
    /**
     * 带回退机制的下载方法（按优先级尝试多个代理，最后回退到原始 URL）
     */
    private Path downloadSingleWithFallback(UrlInfo urlInfo, Path baseDir, boolean extract) throws IOException {
        org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DownloadService.class);
        List<String> candidateUrls = urlInfo.candidateUrls;
        
        IOException lastException = null;
        
        // 按顺序尝试所有候选 URL（包括原始 URL 作为最后一个兜底）
        for (int i = 0; i < candidateUrls.size(); i++) {
            String candidateUrl = candidateUrls.get(i);
            boolean isLast = (i == candidateUrls.size() - 1);
            
            try {
                String fileName = resolveFileName(candidateUrl);
                Path targetFile = baseDir.resolve(fileName);
                
                if (i > 0) {
                    log.info("尝试第 {} 个候选 URL (共 {} 个): {}", i + 1, candidateUrls.size(), candidateUrl);
                }
                
                return attemptDownload(candidateUrl, targetFile, baseDir, extract);
                
            } catch (IOException e) {
                lastException = e;
                
                // 如果是最后一个 URL（原始 URL），直接抛出异常
                if (isLast) {
                    log.error("所有候选 URL 都下载失败（包括原始 GitHub URL）");
                    throw new IOException("所有下载尝试都失败。最后尝试: " + candidateUrl + 
                            ", 错误: " + e.getMessage(), e);
                }
                
                // 不是最后一个，继续尝试下一个
                boolean isProxyError = e.getMessage().contains("HTTP 403") || 
                                     e.getMessage().contains("HTTP 404") ||
                                     e.getMessage().contains("HTTP 5");
                
                if (isProxyError) {
                    log.warn("候选 URL {} 下载失败 ({}): {}, 尝试下一个候选 URL", 
                            i + 1, e.getMessage(), candidateUrl);
                } else {
                    // 非代理错误（如网络错误），也尝试下一个
                    log.warn("候选 URL {} 下载失败 ({}): {}, 尝试下一个候选 URL", 
                            i + 1, e.getMessage(), candidateUrl);
                }
            }
        }
        
        // 理论上不会到达这里，因为至少会尝试原始 URL
        throw new IOException("所有下载尝试都失败", lastException);
    }
    
    /**
     * 执行实际的下载尝试
     */
    private Path attemptDownload(String url, Path targetFile, Path baseDir, boolean extract) throws IOException {
        HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(URI.create(url))
                .timeout(Duration.ofMinutes(10))
                .header("Accept", "*/*")
                .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
        
        HttpRequest request = requestBuilder.GET().build();

        try {
            HttpResponse<InputStream> response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
            if (response.statusCode() >= 200 && response.statusCode() < 300) {
                try (InputStream in = response.body()) {
                    Files.copy(in, targetFile, StandardCopyOption.REPLACE_EXISTING);
                }
            } else {
                throw new IOException("HTTP " + response.statusCode() + " downloading: " + url);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IOException("Interrupted downloading: " + url, e);
        }

        if (extract && isArchive(targetFile)) {
            extractArchive(targetFile, baseDir);
        }

        return targetFile;
    }

    private static String resolveFileName(String url) {
        String path = URI.create(url).getPath();
        if (path == null || path.isEmpty() || path.endsWith("/")) {
            return "download";
        }
        int idx = path.lastIndexOf('/');
        return idx >= 0 ? path.substring(idx + 1) : path;
    }

    private static boolean isArchive(Path file) {
        String name = file.getFileName().toString().toLowerCase();
        return name.endsWith(".zip") || name.endsWith(".tar.gz") || name.endsWith(".tgz");
    }

    private static void extractArchive(Path archive, Path destDir) throws IOException {
        String name = archive.getFileName().toString().toLowerCase();
        if (name.endsWith(".zip")) {
            extractZip(archive, destDir);
        } else if (name.endsWith(".tar.gz") || name.endsWith(".tgz")) {
            extractTgz(archive, destDir);
        }
    }

    private static void extractZip(Path zipFile, Path destDir) throws IOException {
        Files.createDirectories(destDir);
        try (InputStream fis = Files.newInputStream(zipFile);
             ZipInputStream zis = new ZipInputStream(fis)) {
            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                Path outPath = destDir.resolve(entry.getName()).normalize();
                if (!outPath.startsWith(destDir)) {
                    throw new IOException("Entry is outside the target dir: " + entry.getName());
                }
                if (entry.isDirectory()) {
                    Files.createDirectories(outPath);
                } else {
                    Files.createDirectories(outPath.getParent());
                    Files.copy(zis, outPath, StandardCopyOption.REPLACE_EXISTING);
                }
                zis.closeEntry();
            }
        }
    }

    private static void extractTgz(Path tgzFile, Path destDir) throws IOException {
        Files.createDirectories(destDir);
        try (InputStream fis = Files.newInputStream(tgzFile);
             GzipCompressorInputStream gis = new GzipCompressorInputStream(fis);
             TarArchiveInputStream tis = new TarArchiveInputStream(gis)) {
            ArchiveEntry entry;
            while ((entry = tis.getNextEntry()) != null) {
                Path outPath = destDir.resolve(entry.getName()).normalize();
                if (!outPath.startsWith(destDir)) {
                    throw new IOException("Entry is outside the target dir: " + entry.getName());
                }
                if (entry.isDirectory()) {
                    Files.createDirectories(outPath);
                } else {
                    Files.createDirectories(outPath.getParent());
                    try (var out = Files.newOutputStream(outPath)) {
                        IOUtils.copy(tis, out);
                    }
                }
            }
        }
    }
}


